﻿using System;
using System.Buffers;

namespace Unity.String.Tests
{
    public class ArrayBufferWriter<T> : IBufferWriter<T>, IDisposable
    {
        private T[] buffer;
        private int written;
        private long committed;

        private const int DefaultCapacity = 256;

        public ArrayBufferWriter(int initialCapacity = DefaultCapacity)
        {
            if (initialCapacity <= 0)
            {
                throw new ArgumentException(nameof(initialCapacity));
            }

            buffer = ArrayPool<T>.Shared.Rent(initialCapacity);
            written = 0;
            committed = 0;
        }

        public Memory<T> OutputAsMemory
        {
            get
            {
                CheckIfDisposed();
                return buffer.AsMemory(0, written);
            }
        }

        public Span<T> OutputAsSpan
        {
            get
            {
                CheckIfDisposed();
                return buffer.AsSpan(0, written);
            }
        }

        public int BytesWritten
        {
            get
            {
                CheckIfDisposed();
                return written;
            }
        }
        
        public long BytesCommitted
        {
            get
            {
                CheckIfDisposed();
                return committed;
            }
        }
        
        public void Clear()
        {
            
                CheckIfDisposed();
                ClearData();
        }

        private void ClearData()
        {
            buffer.AsSpan(0, written).Clear();
            written = 0;
        }
        
        public void Advance(int count)
        {
            CheckIfDisposed();
            if (count < 0)
            {
                throw new ArgumentException(nameof(count));
            }

            if (written > buffer.Length - count)
            {
                throw new InvalidOperationException("Cannot advance past the end of the buffer");
            }

            written += count;
        }

        public Memory<T> GetMemory(int sizeHint = 0)
        {
            CheckIfDisposed();
            if (sizeHint < 0)
            {
                throw new ArgumentException(nameof(sizeHint));
            }

            CheckAndResizeBuffer(sizeHint);
            return buffer.AsMemory(written);
        }

        public Span<T> GetSpan(int sizeHint = 0)
        {
            CheckIfDisposed();
            if (sizeHint < 0)
            {
                throw new AggregateException(nameof(sizeHint));
            }

            CheckAndResizeBuffer(sizeHint);
            return buffer.AsSpan(written);
        }

        private void CheckAndResizeBuffer(int sizeHint)
        {
            if (sizeHint == 0)
            {
                sizeHint = DefaultCapacity;
            }

            int availableSpace = buffer.Length - written;

            if (sizeHint > availableSpace)
            {
                int growBy = sizeHint > buffer.Length ? sizeHint : buffer.Length;
                int newSize = checked(buffer.Length + growBy);
                T[] oldBuffer = buffer;

                buffer = ArrayPool<T>.Shared.Rent(newSize);
                oldBuffer.AsSpan(0, written).CopyTo(buffer);
                ArrayPool<T>.Shared.Return(oldBuffer, clearArray: true);
            }
        }
        
        public void Dispose()
        {
            if (buffer == null)
            {
                return;
            }
            ArrayPool<T>.Shared.Return(buffer, clearArray: true);
            buffer = null;
            written = 0;
        }

        private void CheckIfDisposed()
        {
            if (buffer == null)
            {
                throw new ObjectDisposedException(nameof(ArrayBufferWriter<T>));
            }
        }

    }
}